package org.koron.ebs.mybatis;

import org.apache.ibatis.datasource.pooled.PooledDataSource;
import org.apache.ibatis.logging.log4j2.Log4j2Impl;
import org.apache.ibatis.mapping.Environment;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.apache.ibatis.transaction.TransactionFactory;
import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.sql.DataSource;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.*;

public class SessionFactory implements AutoCloseable {
    /**
     * 数据库连接用账号
     */
    public static final String PROPERTY_USER = "user";
    /**
     * 数据库连接用密码
     */
    public static final String PROPERTY_PASSWORD = "password";
    /**
     * 数据库连接用URL
     */
    public static final String PROPERTY_URL = "url";
    /**
     * 数据库连接用driver
     */
    public static final String PROPERTY_DRIVER = "driver";
    /**
     * 数据库连接池最大空闲连接
     */
    public static final String PROPERTY_MAXIDLE = "maxIdle";
    /**
     * 数据库连接池最大连接数
     */
    public static final String PROPERTY_MAXTOTAL = "maxTotal";
    /**
     * 用来存数据库链接
     */
    private transient static HashMap<String, SqlSessionFactory> factoryMap = new HashMap<String, SqlSessionFactory>();
    /**
     * dataSource
     */
    private transient static HashMap<String, DataSource> ds = new HashMap<>();
    /**
     * 缓存JNDI
     */
    private transient static HashMap<String, String> jndi = new HashMap<String, String>();
    /**
     * 缓存直连
     */
    private transient static HashMap<String, Properties> pool = new HashMap<String, Properties>();
    /**
     * 缓存切入器
     */
    private transient static Map<String, List<Class<Interceptor>>> interceptorMap = new HashMap<>();
    /**
     * 用来存env对应连接的表
     */
    private Map<String, SqlSession> envMap = new HashMap<>();
    /**
     * 类监听器,每次加载类均会触发此接口的process方法 {@link ClassListener}
     */
    public static ClassListener listener = null;
    /**
     * 日志
     */
    private static Logger logger = LogManager.getLogger(SessionFactory.class.getName());

    private static String logImpl = "org.apache.ibatis.logging.log4j.Log4j2Impl";
    private static boolean mapUnderscoreToCamelCase = false;
    /**
     * 默认获取mapper所使用的连接别名。 为NULL时不强制使用别名。
     */
    private String dbEnv;
    /**
     * @deprecated 使用 {@link ADOConnection}取代
     */
    @Deprecated
    public SessionFactory() {

    }
    /**
     * 获取Mapper实例
     *
     * @param clazz Mapper类
     * @param env 环境，如果为null那么使用缺省值连接
     * @return Mapper实例
     */
    public <T> T getMapper(Class<T> clazz, final String env) {
        return getMapper(clazz,env,false);
    }
    /**
     * 获取Mapper实例
     *
     * @param clazz Mapper类
     * @param env 环境，如果为null那么使用缺省值连接
     * @param annoFirst 注解优先.如果env存在，同时mapper有注解，则以注解优先。
     * @return Mapper实例
     */
    public synchronized <T> T getMapper(Class<T> clazz, final String env, final boolean annoFirst) {
        logger.debug(clazz.getName());
        String environment = null;
        if (annoFirst) {
            EnvSource ds = clazz.getAnnotation(EnvSource.class);
            if (ds != null)
                environment = ds.value();
        }
        if (environment == null)
            environment = env;
        if (environment == null) {// 如果没指定env,则从类声明中取出env
            EnvSource ds = clazz.getAnnotation(EnvSource.class);
            if (ds != null)
                environment = ds.value();
        }
        // 如果取不到ENV，则使用默认连接
        if (environment == null)
            environment = EnvSource.DEFAULT;
        if (!factoryMap.containsKey(environment))
            throw new RuntimeException("数据环境:" + environment + "未被注册");

        SqlSession session = envMap.get(environment);
        if (session == null) {
            session = factoryMap.get(environment).openSession();
            envMap.put(environment, session);
        }
        // fixup: MapperRegistry.addMapper 方法执行完54行后，56行前，若有第二个线程到此判断，会跳过registeClazz，但此时xml尚未解析完成，故此处一起放在同步锁里
        if (!factoryMap.get(environment).getConfiguration().hasMapper(clazz))// 如果此类未注册则自动注册
            registeClazz(environment, clazz);
        T t = session.getMapper(clazz);
        return t;
    }

    /**
     * 获取Mapper
     *
     * @param c 接口
     * @return
     */
    public <T> T getMapper(Class<T> c) {
        if (dbEnv == null)
            return getMapper(c, null);
        else
            return getMapper(c, dbEnv,true);
    }

    /**
     * 关闭所有session
     *
     * @param commit 是否执行提交命令。为false执行rollback。
     */
    public void close(boolean commit) {
        for (SqlSession session : envMap.values()) {
            if (commit)
                session.commit();
            else
                session.rollback();
            session.close();
        }
        envMap.clear();
    }

    /**
     * 提交所有连接，并关闭所有连接。
     */
    @Override
    public void close() {
        close(true);
    }

    /**
     * <pre>
     * 运行SQL任务.
     * 如果不发生异常会自动提交SQL语句，否则回滚
     *
     * </pre>
     *
     * @param task
     * @deprecated 使用 {@link ADOConnection#runTask(SqlTask, Class)} 代替
     */
    @SuppressWarnings({ "unchecked" })
    @Deprecated
    public <T> T runTask(SqlTask task, Class<T> c) {
        if(c == null)
            throw new NullPointerException("Class can't be null.");
        T t = null;
        try {
            Object o = task.run(this);
            if (o == null || c.isInstance(o)) {
                t = (T) o;
            } else {
                throw new IllegalArgumentException("返回的参数异常");
            }
            close();
        } catch (Exception ex) {
            close(false);
            throw new RuntimeException(ex.getMessage(),ex);
        }
        return t;
    }

    /**
     * 运行SQL任务.需要配合 {@link TaskAnnotation}使用
     *
     * @param object 运行方法所属的实例
     * @param tag SQL标签
     * @param c 运行任务要返回实例所属的类
     * @param objs 运行方法所需的参数
     * @return 任务运行后的对象
     * @see TaskAnnotation
     * @deprecated 使用 {@link ADOConnection#runTask(String, Object, String, Class, Object...)} 代替
     */
    @SuppressWarnings("unchecked")
    @Deprecated
    public <T> T runTask(Object object, String tag, Class<T> c, Object... objs) {
        if(c == null)
            throw new NullPointerException("Class can't be null.");
        T t = null;
        Class<?> clazz = null;
        if (object instanceof Class)// 如果是类,则调用的标签是静态方法
            clazz = (Class<?>) object;
        else
            clazz = object.getClass();
        for (Method method : clazz.getDeclaredMethods()) {
            if (method.getAnnotation(TaskAnnotation.class) != null && method.getAnnotation(TaskAnnotation.class).value().equals(tag)) {
                try {
                    Object[] tmp = new Object[method.getGenericParameterTypes().length];
                    tmp[0] = this;
                    System.arraycopy(objs, 0, tmp, 1, method.getGenericParameterTypes().length - 1);
                    method.setAccessible(true);
                    Object o = null;
                    if ((method.getModifiers() & Modifier.STATIC) != 0)// 如果是静态方法则用null来调用
                        o = method.invoke(null, tmp);
                    else if (!(object instanceof Class))
                        o = method.invoke(object, tmp);
                    if (o == null || c.isAssignableFrom(o.getClass())) {
                        t = (T) o;
                    } else {
                        throw new IllegalArgumentException("返回的参数异常");
                    }
                    close();
                } catch (Exception e) {
                    close(false);
                    throw new RuntimeException(e.getMessage(),e);
                }
                return t;
            }
        }
        return null;
    }

    /**
     * 注册JNDI连接
     *
     * @param env 环境名，Mybatis调用，缺省连接为 {@link EnvSource#DEFAULT}
     * @param jndi JNDI名
     */
    protected static void registeJndiMap(String env, String jndi) {
        try {
            Context ic = new InitialContext();
            DataSource dataSource = (DataSource) ic.lookup(jndi);
            initFactory(env, dataSource);
            SessionFactory.jndi.put(env, jndi);
        } catch (NamingException e) {
            logger.error(e.getMessage(),e);
        }
    }

    /**
     * 注册连接
     *
     * @param env 环境名，Mybatis调用，缺省连接为 {@link EnvSource#DEFAULT}
     * @param dataSource 数据库连接池
     */
    protected static void registeDataSourceMap(String env, DataSource dataSource) {
        initFactory(env, dataSource);
        SessionFactory.ds.put(env, dataSource);
    }

    /**
     * 注册数据JDBC数据库连接 属性中：url为数据库连接的地址，driver为驱动,user为账号,password为密码
     *
     * @param env mybatis的环境
     * @param prop 属性。
     */
    protected static void registeDBMap(String env, Properties prop) {
        String driver = prop.getProperty(SessionFactory.PROPERTY_DRIVER);
        if (driver == null)
            logger.error("You haven't driver property of DB");
        String url = prop.getProperty(SessionFactory.PROPERTY_URL);
        if (url == null)
            logger.error("You haven't url property of DB");
        if (prop.getProperty(SessionFactory.PROPERTY_USER) == null)
            logger.debug("You haven't set user property of DB");
        if (prop.getProperty(SessionFactory.PROPERTY_PASSWORD) == null)
            logger.debug("You haven't set password property of DB");
        PooledDataSource dataSource = new PooledDataSource(driver, url, prop);
        Object o = prop.get(PROPERTY_MAXIDLE);
        int defaultMaxIdle = 2;
        try {
            if (o != null)
                defaultMaxIdle = Integer.parseInt(String.valueOf(o));
        } catch (NumberFormatException ex) {
            logger.debug("Max idle connection isn't set as number. It will set to 2 as default.");
        }
        dataSource.setPoolMaximumIdleConnections(defaultMaxIdle);
        o = prop.get(PROPERTY_MAXTOTAL);
        int defaultMaxConnection = 200;
        try {
            if (o != null)
                defaultMaxConnection = Integer.parseInt(String.valueOf(o));
        } catch (NumberFormatException ex) {
            logger.debug("Max active connection isn't set as number. It will set to 200 as default.");
        }
        dataSource.setPoolMaximumActiveConnections(defaultMaxConnection);
        initFactory(env, dataSource);
        pool.put(env, prop);
    }

    private static void initFactory(String env, DataSource dataSource) {
        TransactionFactory transactionFactory = new JdbcTransactionFactory();
        Environment environment = new Environment(env, transactionFactory, dataSource);
        Configuration configuration = new Configuration(environment);
        configuration.setLogImpl(Log4j2Impl.class);
        configuration.setMapUnderscoreToCamelCase(mapUnderscoreToCamelCase);
        if (interceptorMap.get(env) != null) {
            for (Class<Interceptor> interceptor : interceptorMap.get(env)) {
                try {
                    configuration.addInterceptor(interceptor.newInstance());
                } catch (InstantiationException | IllegalAccessException e) {
                    logger.error("初始化切入器异常，无不带参数构造函数", e);
                }
            }
        }
        factoryMap.put(env, new SqlSessionFactoryBuilder().build(configuration));
    }

    /**
     * 移除数据环境
     *
     * @param env 数据环境标识
     */
    protected static void removeEnvSource(String env) {
        factoryMap.remove(env);
    }

    /**
     * 注册mybatis使用的类
     *
     * @param env mybatis的环境
     * @param clazz 注册的类
     */
    protected static void registeClazz(String env, Class<?> clazz) {
        if (!factoryMap.containsKey(env))
            throw new RuntimeException("数据环境:" + env + "未被注册");
        SqlSessionFactory factory = factoryMap.get(env);
        if (!factory.getConfiguration().hasMapper(clazz)) {
            logger.info("加载类" + clazz.getName());
            factory.getConfiguration().addMapper(clazz);
            if (listener != null)
                listener.process(clazz);
        }
    }

    /**
     * 重新刷mybatis配置，只重新建连接，mapper不进行映射
     */
    public static void refresh() {
        factoryMap = new HashMap<>();
        for (Map.Entry<String, String> entry : jndi.entrySet()) {
            registeJndiMap(entry.getKey(), entry.getValue());
        }
        for (Map.Entry<String, Properties> entry : pool.entrySet()) {
            registeDBMap(entry.getKey(), entry.getValue());
        }
        for (Map.Entry<String, DataSource> entry : ds.entrySet()) {
            registeDataSourceMap(entry.getKey(), entry.getValue());
        }
    }

    /**
     * 添加mybatis切片
     *
     * @param env 环境
     * @param interceptor 切片
     * @return
     */
    @SuppressWarnings("unchecked")
    protected static boolean addInterceptor(String env, String interceptor) {
        try {
            @SuppressWarnings("rawtypes")
            Class clazz = Class.forName(interceptor);
            if (Interceptor.class.isAssignableFrom(clazz))// 如果类是Interceptor的子类则返回true
            {
                if (interceptorMap.get(env) == null) {
                    interceptorMap.put(env, new ArrayList<>());
                }
                interceptorMap.get(env).add(clazz);
                return true;
            } else {
                logger.error(interceptor + " isn't Interceptor.");
            }
        } catch (ClassNotFoundException e) {
            logger.error(interceptor + " isn't a Class", e);
        }
        return false;
    }

    /**
     * 移除切片器
     *
     * @param env 数据环境
     * @param interceptor 切片器
     */
    protected static void removeInterceptor(String env, String interceptor) {
        if (interceptorMap.get(env) == null)
            return;
        for (Class<Interceptor> clazz : interceptorMap.get(env)) {
            if (clazz.getName().equals(interceptor)) {
                interceptorMap.get(env).remove(clazz);
                return;
            }
        }
        return;
    }

    /**
     * @return the logImpl
     */
    public static String getLogImpl() {
        return logImpl;
    }

    /**
     * @param logImpl the logImpl to set
     */
    public static void setLogImpl(String logImpl) {
        SessionFactory.logImpl = logImpl;
    }

    /**
     * @return the mapUnderscoreToCamelCase
     */
    public static boolean isMapUnderscoreToCamelCase() {
        return mapUnderscoreToCamelCase;
    }

    /**
     * @param mapUnderscoreToCamelCase the mapUnderscoreToCamelCase to set
     */
    public static void setMapUnderscoreToCamelCase(boolean mapUnderscoreToCamelCase) {
        SessionFactory.mapUnderscoreToCamelCase = mapUnderscoreToCamelCase;
    }
    /**
     * @return the dbEnv
     */
    public String getDbEnv() {
        return dbEnv;
    }
    /**
     * @param dbEnv the dbEnv to set
     */
    public void setDbEnv(String dbEnv) {
        this.dbEnv = dbEnv;
    }
}
